﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Taxonomy;
using NLog;

namespace UserProfileSync
{
	class Program
	{
		private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
		private const string MULTI_VALUE_TYPE = "string (Multi Value)";

		/// <summary>
		/// 
		/// </summary>
		/// <param name="args">
		/// -export
		/// -import
		/// -url "http://site.url"
		/// -filename "c:\properties"
		/// </param>
		
		static void Main(string[] args)
		{
			var arguments = new Arguments(args);

			// first make sure we have necessary arguments
			bool bExport = arguments["export"] == "true";
			bool bImport = arguments["import"] == "true";
			string filename = arguments["filename"];

			if ((!bExport && !bImport) || (bExport && bImport) || String.IsNullOrEmpty(filename))
			{
				Console.WriteLine(ShowUse());
				return;
			}

			string _url = GetFarmUrl();

			if (_url == null)
			{
				Logger.Error("Local SharePoint farm not found.");
				return;
			}

			SPSite site = null;

			SPSecurity.RunWithElevatedPrivileges(() =>
			{
				site = new SPSite(_url);
			});

			if (bExport)
			{
				Logger.Info("Exporting User Profile properties to " + filename);

				try
				{
					ExportUserProfileProperties(site, filename);
				}
				catch (Exception ex)
				{
					Logger.ErrorException("Failed to Export User Profile Properties", ex);
				}

				Logger.Info("Done.");
			}
			else if (bImport)
			{
				Logger.Info("Importing User Profile properties from " + filename);

				try
				{
					ImportUserProfileProperties(site, filename);
				}
				catch (Exception ex)
				{
					Logger.ErrorException("Failed to Import User Profile Properties", ex);
				}
				

				Logger.Info("Done.");
			}

			site.Dispose();
		}

		static string ShowUse()
		{
			string use = String.Format(@"
User Profile Properties Synchronization.
NOTE: Utility has to run on a computer that is a part of the sharepoint farm.

Use:
uppsync.exe -import|-export -filename
");
			return use;
		}

		private static void ExportUserProfileProperties(SPSite site, string filename)
		{
			SPServiceContext context = SPServiceContext.GetContext(site);
			var userProfileConfigManager = new UserProfileConfigManager(context);
			ProfilePropertyManager profilePropertyManager = userProfileConfigManager.ProfilePropertyManager;
			CorePropertyManager corePropertyManager = profilePropertyManager.GetCoreProperties();
			ProfileTypePropertyManager ptpm = profilePropertyManager.GetProfileTypeProperties(ProfileType.User);
			ProfileSubtypeManager psm = ProfileSubtypeManager.Get(context);
			ProfileSubtype ps = psm.GetProfileSubtype(ProfileSubtypeManager.GetDefaultProfileName(ProfileType.User));
			ProfileSubtypePropertyManager pspm = ps.Properties;
			
			var properties = corePropertyManager.ToList();

			var xdocument = SerializeProperties(properties, ptpm, pspm);

			xdocument.Save(filename);
		}

		static XDocument SerializeProperties(IEnumerable<CoreProperty> properties, ProfileTypePropertyManager ptpm, ProfileSubtypePropertyManager pspm)
		{
			var xdocument = new XDocument();

			var xproperties = new XElement("UserProfileProperties");
			xdocument.Add(xproperties);

			foreach (var coreProperty in properties)
			{
				var xproperty = new XElement("UserProfileProperty");
				
				AddAttribute(xproperty,"Name", coreProperty.Name);
				AddAttribute(xproperty,"DisplayName", coreProperty.DisplayName);
				//AddAttribute(xproperty,"DisplayNameLocalized", coreProperty.DisplayNameLocalized); // TODO
				AddAttribute(xproperty,"Description", coreProperty.Description);
				//AddAttribute(xproperty,"DescriptionLocalized", coreProperty.DescriptionLocalized); // TODO
				AddAttribute(xproperty,"IsAlias", coreProperty.IsAlias);
				AddAttribute(xproperty,"IsSearchable", coreProperty.IsSearchable);
				AddAttribute(xproperty,"IsSection", coreProperty.IsSection);
				AddAttribute(xproperty,"Length", coreProperty.Length);
				AddAttribute(xproperty,"Type", coreProperty.Type);
				AddAttribute(xproperty,"Separator", coreProperty.Separator);

				if (coreProperty.Type == MULTI_VALUE_TYPE && coreProperty.TermSet != null)
				{
					AddAttribute(xproperty, "TermSetName", coreProperty.TermSet.Name);
					AddAttribute(xproperty, "TermSetGroupName", coreProperty.TermSet.Group.Name);
					AddAttribute(xproperty,"TermSetStoreName", coreProperty.TermSet.TermStore.Name);
				}

				// Profile type properties
				var ptp = ptpm.GetPropertyByName(coreProperty.Name);
				if (ptp != null)
				{
					AddAttribute(xproperty, "IsReplicable", ptp.IsReplicable);
					AddAttribute(xproperty, "IsVisibleOnEditor", ptp.IsVisibleOnEditor);
					AddAttribute(xproperty, "IsVisibleOnViewer", ptp.IsVisibleOnViewer);
					AddAttribute(xproperty, "MaximumShown", ptp.MaximumShown);
				}

				// Profile subtype properties
				var psp = pspm.GetPropertyByName(coreProperty.Name);
				if (psp != null)
				{
					AddAttribute(xproperty, "PrivacyPolicy", psp.PrivacyPolicy);
					AddAttribute(xproperty, "DefaultPrivacy", psp.DefaultPrivacy);
					AddAttribute(xproperty, "IsUserEditable", psp.IsUserEditable);
					AddAttribute(xproperty, "UserOverridePrivacy", psp.UserOverridePrivacy);
				}

				xproperties.Add(xproperty);
			}

			return xdocument;
		}

		private static void ImportUserProfileProperties(SPSite site, string filename)
		{
			SPServiceContext context = SPServiceContext.GetContext(site);
			var userProfileConfigManager = new UserProfileConfigManager(context);
			ProfilePropertyManager profilePropertyManager = userProfileConfigManager.ProfilePropertyManager;
			CorePropertyManager corePropertyManager = profilePropertyManager.GetCoreProperties();
			ProfileTypePropertyManager profileTypePropertyManager = profilePropertyManager.GetProfileTypeProperties(ProfileType.User);
			ProfileSubtypeManager psm = ProfileSubtypeManager.Get(context);
			ProfileSubtype ps = psm.GetProfileSubtype(ProfileSubtypeManager.GetDefaultProfileName(ProfileType.User));
			ProfileSubtypePropertyManager profileSubtypePropertyManager = ps.Properties;

			var xdocument = XDocument.Load(filename);

			var xproperties = xdocument.Element("UserProfileProperties");

			if (xproperties == null) return;

			foreach (var xproperty in xproperties.Descendants("UserProfileProperty"))
			{
				string name = GetAttribute(xproperty, "Name");
				string displayName = GetAttribute(xproperty, "DisplayName");
				string description = GetAttribute(xproperty, "Description");
				bool isAlias = GetAttribute<bool>(xproperty, "IsAlias");
				bool isSearcheable = GetAttribute<bool>(xproperty, "IsSearchable");
				bool isSection = GetAttribute<bool>(xproperty, "IsSection");
				int length = GetAttribute<int>(xproperty, "Length");
				string type = GetAttribute(xproperty, "Type");
				bool isMultivalued = type == MULTI_VALUE_TYPE;
				MultiValueSeparator separator = GetAttribute<MultiValueSeparator>(xproperty, "Separator");
				string termSetName = GetAttribute(xproperty, "TermSetName");
				string termSetGroupName = GetAttribute(xproperty, "TermSetGroupName");
				string termSetStoreName = GetAttribute(xproperty, "TermSetStoreName");
				// ProfileTypeProperty
				bool isReplicable = GetAttribute<bool>(xproperty, "IsReplicable");
				bool isVisibleOnEditor = GetAttribute<bool>(xproperty, "IsVisibleOnEditor");
				bool isVisibleOnViewer = GetAttribute<bool>(xproperty, "IsVisibleOnViewer");
				int maximumShown = GetAttribute<int>(xproperty, "MaximumShown");
				// Profile subtype property
				PrivacyPolicy privacyPolicy = GetAttribute <PrivacyPolicy>(xproperty, "PrivacyPolicy");
				Privacy defaultPrivacy = GetAttribute<Privacy>(xproperty, "DefaultPrivacy");
				bool isUserEditable = GetAttribute<bool>(xproperty, "IsUserEditable");
				bool isUserOverridePrivacy = GetAttribute<bool>(xproperty, "UserOverridePrivacy");

				// check if property exists
				var coreProperty = corePropertyManager.GetPropertyByName(name);

				bool exist = coreProperty != null;

				if (!exist)
				{
					coreProperty = corePropertyManager.Create(isSection);
					coreProperty.Name = name;
					coreProperty.Type = type;
					coreProperty.IsMultivalued = isMultivalued;
					coreProperty.Length = length;
				}
				else
				{
					// we don't update existign property if it's of different type
					if (coreProperty.Type != type) continue;

					if (coreProperty.IsMultivalued && !isMultivalued)
					{
						Logger.Warn("Property {0} of type {1} can not be multivalued. Unable to update.", name, type);
						continue;
					}
				}

				coreProperty.DisplayName = displayName;
				coreProperty.Description = description;
				coreProperty.IsAlias = isAlias;
				coreProperty.IsSearchable = isSearcheable;
				coreProperty.Separator = separator;

				if (isMultivalued && !String.IsNullOrEmpty(termSetName))
				{
					try
					{
						TaxonomySession session = new TaxonomySession(site);

						if (session.TermStores.Count > 0)
						{
							TermStore termStore = session.TermStores.FirstOrDefault(ts => ts.Name == termSetStoreName) ?? session.TermStores[0];
						
							Group group = termStore.Groups.FirstOrDefault(gr => gr.Name == termSetGroupName);

							if (group != null)
							{
								var termSet = group.TermSets.FirstOrDefault(ts => ts.Name == termSetName);

								if (termSet != null)
								{
									coreProperty.TermSet = termSet;
								}
								else
								{
									Logger.Warn("Unable to associate the termSet (Name={0}, Group={1}, TermStore={2}) with the user property {3} - TermSet doesn't exist.", termSetName, termSetGroupName, termSetStoreName, name);
								}
							}
							else
							{
								Logger.Warn("Unable to associate the termSet (Name={0}, Group={1}, TermStore={2}) with the user property {3} - Group doesn't exist.", termSetName, termSetGroupName, termSetStoreName, name);
							}
						}
						else
						{
							Logger.Warn("Unable to associate the termSet (Name={0}, Group={1}, TermStore={2}) with the user property {3} - Term Store doesn't exist.", termSetName, termSetGroupName, termSetStoreName, name);
						}
					}
					catch (Exception ex)
					{
						Logger.ErrorException(String.Format("Unable to associate the termSet (Name={0}, Group={1}, TermStore={2}) with the user property {3}", termSetName, termSetGroupName, termSetStoreName, name), ex);
					}
				}

				// Commit changes
				if (exist)
				{
					try
					{
						coreProperty.Commit();
						Logger.Info("Property {0} updated.", name);
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to commit changes to property '"+ coreProperty.Name +"'", ex);
						continue;
					}
				}
				else
				{
					try
					{
						corePropertyManager.Add(coreProperty);
						Logger.Info("Property {0} added.", name);
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to add property '" + coreProperty.Name + "'", ex);
						continue;
					}
				}

				// Set profile type properties
				var profileTypeProperty = profileTypePropertyManager.GetPropertyByName(coreProperty.Name);
				bool ptp_exists = profileTypeProperty != null;
				if (!ptp_exists )
				{
					profileTypeProperty = profileTypePropertyManager.Create(coreProperty);
					if (isMultivalued)
						profileTypeProperty.MaximumShown = maximumShown;
				}
				profileTypeProperty.IsReplicable = isReplicable;
				profileTypeProperty.IsVisibleOnEditor = isVisibleOnEditor;
				profileTypeProperty.IsVisibleOnViewer = isVisibleOnViewer;

				if (ptp_exists)
				{
					try
					{
						profileTypeProperty.Commit();
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to update profile type property '" + profileTypeProperty.Name + "'", ex);
						continue;
					}
				}
				else
				{
					try
					{
						profileTypePropertyManager.Add(profileTypeProperty);
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to add profile type property '" + profileTypeProperty.Name + "'", ex);
						continue;
					}
				}

				// Set profile subtype properties
				var profileSubtypeProperty = profileSubtypePropertyManager.GetPropertyByName(coreProperty.Name);
				bool psp_exists = profileSubtypeProperty != null;
				if (!psp_exists)
				{
					profileSubtypeProperty = profileSubtypePropertyManager.Create(profileTypeProperty);
					//psp.Name = coreProperty.Name;
				}

				profileSubtypeProperty.PrivacyPolicy = privacyPolicy;
				profileSubtypeProperty.DefaultPrivacy = defaultPrivacy;
				profileSubtypeProperty.IsUserEditable = isUserEditable;
				profileSubtypeProperty.UserOverridePrivacy = isUserOverridePrivacy;

				if (psp_exists)
				{
					try
					{
						profileSubtypeProperty.Commit();
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to update profile subtype property '" + profileSubtypeProperty.Name + "'", ex);
						continue;
					}
				}
				else
				{
					try
					{
						profileSubtypePropertyManager.Add(profileSubtypeProperty);
					}
					catch (Exception ex)
					{
						Logger.ErrorException("Failed to add profile type property '" + profileTypeProperty.Name + "'", ex);
						continue;
					}
				}
			}
		}
		

		static void AddAttribute(XElement xelement, string attributeName, object value)
		{
			if (value == null) return;
			xelement.Add(new XAttribute(attributeName, value));
		}

		static T GetAttribute<T>(XElement xelement, string attributeName)
		{
			if (xelement == null) return default(T);

			var xattribute = xelement.Attribute(attributeName);

			if (xattribute == null) return default(T);

			object value = xattribute.Value;

			if (typeof(T) == typeof(bool))
			{
				bool temp;
				Boolean.TryParse(value.ToString(), out temp);
				value = temp;
			}
			else if (typeof(T).BaseType == typeof(Enum))
			{
				try
				{
					value = Enum.Parse(typeof(T), value.ToString());
				}
				catch (Exception)
				{
					value = null;
				}
			}
			else if (typeof(T) == typeof(int))
			{
				try
				{
					value = int.Parse(value.ToString());
				}
				catch (Exception)
				{
					value = default(T);
				}
			}

			return (T)value;
		}

		static string GetAttribute(XElement xelement, string attributeName)
		{
			return GetAttribute<string>(xelement, attributeName);
		}

		static string GetFarmUrl()
		{
			SPFarm farm = SPFarm.Local;

			if (farm == null) return null;

			var UrlCollectionMgr = farm.AlternateUrlCollections;

			foreach (var urlCollection in UrlCollectionMgr)
			{
				foreach (var url in urlCollection.Where(url => url.UrlZone == SPUrlZone.Default))
				{
					return url.Uri.AbsoluteUri;
				}
			}

			return null;
		}
	}
}
